//
// Created by LiuYou on 2021/10/22.
//

#include "MyTbf.hpp"
#include <cstdlib>
#include <unistd.h>
#include <algorithm>
#include <csignal>
#include <pthread.h>
#include <cstdio>
#include <cstring>


class MyTbf {
public:
    MyTbf() = default;

    ~MyTbf() = default;

public:
//    MyTbf() {
//        pthread_mutex_init( &mutex, nullptr );
//    }
//
//    ~MyTbf() {
//        // 如果 mutex的销毁写在析构函数中在当前的设计中是不行的。
//        // 因为是使用 malloc();分配的内存, 使用 free();释放内存。
//        // free();不会调用析构函数。
//        // 如果把 malloc();和free();换成new和delete就可以了,
//        // 这使用了C++的 RAII机制。
//        //
//        // 所以那就在 malloc();之后对mutex进行初始化,
//        // 在 free();之前对mutex进行析构。
//        pthread_mutex_destroy( &mutex );
//    }

public:
//    MyTbf() = default;
//
//    ~MyTbf() = default;

public:
    int cps{};
    int burst{};
    int token{};
    int pos{};

public:
    /*
     * 加锁保护字段 token。
     */
    pthread_mutex_t mutex{};

};


/*
 * 这个策略机制数组对象, 是会造成线程竞争, 所以需要加锁保护。
 * 这个变量/对象是一个独占的资源使用。
 */
static MyTbf* tbfPolicyArray[MY_TBF_MAX];
// todo 笔记: 互斥量的命名体现了这个互斥量在加锁保护哪个变量/对象。
// 那么在哪销毁这个互斥量呢?
// 肯定不能在 tbfDestroy();中销毁, 因为这个函数是销毁一个令牌桶, 不能因为销毁了一个令牌桶就将互斥量销毁了。
// 所以这个互斥量的销毁一定是在模块卸载的时候, 即 moduleUnload();中。
static pthread_mutex_t MUTEX_TBF_POLICY_ARRAY = PTHREAD_MUTEX_INITIALIZER;

//static int inited = 0;

//static sighandler_t alarmHandlerSave;


static pthread_t tidAlarm;

//static pthread_mutex_t MUTEX_MODULE_LOAD_INITED = PTHREAD_MUTEX_INITIALIZER;

static pthread_once_t INIT_ONCE = PTHREAD_ONCE_INIT;


//static bool isLoop = true;


/*
 *todo 笔记: 这个函数在 tbfInit(); 函数中加锁调用了。
 * 但是单独调用这个函数的话使用加锁的。因为这个函数内部使用了 tbfPolicyArray[]数组。
 * 而不能对这个函数内部的 tbfPolicyArray[]数组加锁, 因为 这个函数在 tbfInit();函数中
 * 在临界区内调用了, 如果 对这个函数内部的 tbfPolicyArray[]数组加锁 的话就是
 * 我锁我自己。所以这个函数内部不能使用互斥量, 即不能加锁。所以这个函数需要在命名上体现出来。
 * 因为这个函数可能会在后期改进的编写的其它函数中使用。
 * +
 * 使用 getFreePosUnlock(); 体现出来这个函数是未加锁的版本。
 * 如果你想用这个函数请先加锁再使用！！！！
 * +
 * 表示这个函数本身没有加锁, 但是调用它是需要加锁的。那么就将函数名末尾加上 Unlocked,
 * 提示用户, 自己先加锁再调用这个函数！
 * +
 * 碰到这种情况时, 只能这样解决。包括标准函数的定义命名也是这种方式。
 */
static int getFreePosUnlocked() {
    for (int i = 0; i < MY_TBF_MAX; ++i) {
        if (tbfPolicyArray[i] == nullptr) {
            return i;
        }
    }
    return -1;
}


/*
 * 把这个函数做成一个线程, 跑起来。
 * 这个线程, 每秒钟执行一次。
 * 可以用任何形式的 sleep();函数来实现。
 */
[[deprecated("在多线程重构令牌桶中已经弃用。")]]
static void alarmHandler(int sig) {
    for (int i = 0; i < MY_TBF_MAX; ++i) {
        if (tbfPolicyArray[i] != nullptr) {
            tbfPolicyArray[i]->token += tbfPolicyArray[i]->cps;
            if (tbfPolicyArray[i]->token > tbfPolicyArray[i]->burst) {
                tbfPolicyArray[i]->token = tbfPolicyArray[i]->burst;
            }
        }
    }
    alarm( 1 );
}


static void moduleUnload() {
//    signal( SIGALRM, alarmHandlerSave );
//    alarm( 0 );


//    isLoop = false;

    // todo 笔记: 这里使用 pthread_cancel();来取消这个 alarm线程。
    pthread_cancel( tidAlarm );


    //region 这样写不好, 因为没有释放掉每个令牌桶的mutex字段互斥量。
//    pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
//    for (int i = 0; i < MY_TBF_MAX; ++i) {
//        free( tbfPolicyArray[i] );
//    }
//    pthread_mutex_unlock( &MUTEX_TBF_POLICY_ARRAY );
    //endregion

    //region 这样写改进了上面的那个问题。
    for (int i = 0; i < MY_TBF_MAX; ++i) {
        if (tbfPolicyArray[i] != nullptr) {
            tbfDestroy( tbfPolicyArray[i] );
        }
    }
    //endregion


    pthread_mutex_destroy( &MUTEX_TBF_POLICY_ARRAY );
//    pthread_mutex_destroy( &MUTEX_MODULE_LOAD_INITED );

    pthread_join( tidAlarm, nullptr );
}


static void moduleLoad() {
////    sighandler_t alarmHandlerSave = signal( SIGALRM, alarmHandler );
//    alarmHandlerSave = signal( SIGALRM, alarmHandler );
//    alarm( 1 );

    // 这里要创建一个线程, 这个线程干的事情就是 信号处理函数alarmHandler 要干的活。
    int errNum = pthread_create(
            &tidAlarm, nullptr,
            [](void* args) -> void* {

                while (/*isLoop*/true) {

                    pthread_testcancel();

                    pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
                    for (int i = 0; i < MY_TBF_MAX; ++i) {
                        /*todo 注意:
                         * 不要把 pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
                         * 加到这里。
                         * 因为 一秒钟 我是给所有的令牌桶加 token, 而不是
                         * 加锁 -> 加令牌 -> 解锁 -> 加锁 -> 加令牌 -> 解锁 -> ...
                         * 而是
                         * 加锁 -> 为所有有效的令牌桶加令牌 -> 解锁 -> 加锁 -> 为所有有效的令牌桶加令牌 -> 解锁 -> ...
                         * 所以, pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
                         * 要写到这个 for循环外面。
                         */
//                        pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
                        if (tbfPolicyArray[i] != nullptr) {

                            // 这两把锁互不影响。
                            pthread_mutex_lock( &tbfPolicyArray[i]->mutex );
                            tbfPolicyArray[i]->token += tbfPolicyArray[i]->cps;
                            if (tbfPolicyArray[i]->token > tbfPolicyArray[i]->burst) {
                                tbfPolicyArray[i]->token = tbfPolicyArray[i]->burst;
                            }
                            pthread_mutex_unlock( &tbfPolicyArray[i]->mutex );

                        }
//                        pthread_mutex_unlock( &MUTEX_TBF_POLICY_ARRAY );
                    }
                    pthread_mutex_unlock( &MUTEX_TBF_POLICY_ARRAY );

                    // 这里用使用CLion上的一点注意事项, 如果当写着写着代码, 往往是比较复杂的逻辑。
                    // CLion的智能提示不能用了。那就退出CLion再重新进入CLion,
                    // 给CLion时间让CLion重新分析一下, 然后就又能使用CLion的智能提示了。

                    // 这里的 sleep(1); 可以用其它的安全的可以移植的sleep();函数来替换,
                    // 这里就先使用sleep(1);了。
//                    sleep( 1 );
                    sleep( 1 );
                }

                pthread_exit( nullptr );
            },
            nullptr );
    if (errNum != 0) {
        fprintf( stderr, "pthread_create(): %s.\n", strerror( errNum ));
        // 这里就是要使用 exit();直接退出。
        // 因为如果每秒加令牌桶的线程都没有创建成功, 那还运行啥?
        exit( EXIT_FAILURE );
    }

    atexit( moduleUnload );
}


Tbf* tbfInit(int cps, int burst) {

    // todo 注意: 这里我一直使用的是一个 静态全局变量 来完成单次模块的加载。
    //  这里没有听懂！！！！ 难道说在多线程中调用 tbfInit();函数
    //  这个模块会加载多次, 一个线程就加载一次?
    //  这个 inited 静态全局对象难道是每个线程都有一个吗?
    // todo 笔记:
    //  现在我想明白了, inited 静态全局对象当然是只有一份,
    //  多个线程共享。
    //  这里是, 如果一个线程判断 !inited 后, 进入 moduleLoad();中后,
    //  另一个线程也判断 !inited 了, 这个线程也会进入 moduleLoad();函数。
    //  这样可能 moduleLoad();函数就会调用多次。
    //  而我的本意是只调用一次！！！！
    //  !!!!!!!!!!!!!!!!!!!!!!
    //  即 inited 这个静态全局变量也需要加锁保护！！！！
    //  !!!!!!!!!!!!!!!!!!!!!!
    //  我重新定义了一把锁。 MUTEX_MODULE_LOAD_INITED
    //  可以这样写, 没问题。
//    pthread_mutex_lock( &MUTEX_MODULE_LOAD_INITED );
//    if (!inited) {
//        moduleLoad();
//        inited = 1;
//    }
//    pthread_mutex_unlock( &MUTEX_MODULE_LOAD_INITED );
    /*
     * 动态模块的单次初始化函数。
     * pthread_once();
     */
    pthread_once( &INIT_ONCE, moduleLoad );



    //region 从临界区代码中 提取的与 独占资源无关的代码, 放在临界区外面。
    MyTbf* anInstance = static_cast<MyTbf*>(malloc( sizeof( MyTbf )));
    if (anInstance == nullptr) {
        return nullptr;
    }

    anInstance->token = 0;
    anInstance->cps = cps;
    anInstance->burst = burst;

    // 在 malloc();之后对锁进行初始化。如果是C++就可以使用 RAII机制。
    pthread_mutex_init( &anInstance->mutex, nullptr );
    //endregion


    /*
     *todo 笔记: 对临界区的优化是一定要做的事情。
     * 临界区应该是越短越好。所以把临界区内无关的代码 放到 临界区外面。
     */
    pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
    // todo 笔记: 函数调用也是临界区内的跳转语句！
    //  关于这里的解决办法, 请看 getFreePosUnlocked();的函数注释说明。
    int pos = getFreePosUnlocked();
    if (pos < 0) {
        // todo 笔记: 另外注意临界区内的所有跳转语句。
        pthread_mutex_unlock( &MUTEX_TBF_POLICY_ARRAY );

        free( anInstance );
        return nullptr;
    }


    anInstance->pos = pos;

    tbfPolicyArray[pos] = anInstance;
    pthread_mutex_unlock( &MUTEX_TBF_POLICY_ARRAY );


    return anInstance;
}

int tbfDestroy(Tbf* tbf) {
    MyTbf* anInstance = static_cast<MyTbf*>(tbf);

    pthread_mutex_lock( &MUTEX_TBF_POLICY_ARRAY );
    tbfPolicyArray[anInstance->pos] = nullptr;
    pthread_mutex_unlock( &MUTEX_TBF_POLICY_ARRAY );

    // 如果是C就要这样写, C++可以使用 RAII机制。
    pthread_mutex_destroy( &anInstance->mutex );
    free( tbf );
    return 0;
}

int fetchToken(Tbf* tbf, int tokens) {
    if (tokens <= 0) {
        return -1;
    }

    MyTbf* anInstance = static_cast<MyTbf*>(tbf);
    //todo 笔记: 在多线程中,这里是如何知道 token值 大于 0 的?
    // 而且在加令牌的线程中也使用了这个token这个字段。
    // 所以, 还需要设计一个互斥量来加锁保护这个token字段。
    // +
    // 那么问题来了, 这个互斥量设计在哪里?
    // 和 MUTEX_TBF_POLICY_ARRAY 这个互斥量一样, 设计成静态全局变量?
    // 如果是这样的话, 这个互斥量来保护token字段, 但是最多支持 1024 个令牌桶。
    // 那就是说 1024 个令牌桶当中保护token字段使用的是同一个互斥量。
    // 这显然是不对的。
    // +
    // 所以这个互斥量应该设计在这个结构体/class内部, 来保护token字段。
    // 这样就保证了class/结构体是线程安全的, 每一个结构体对象在不同的线程
    // 中访问自己的字段是安全的。

    pthread_mutex_lock( &anInstance->mutex );
    /*
     *todo 笔记: 盲等是因为这里！
     * 加锁查看, 解锁出让CPU, 然后在加锁查看, 解锁出让CPU。
     * 一秒钟在这转了上亿次, 然后 alarm线程才加了一次。
     * 然后它拿上了token后, 下面马上就又用了。
     * +
     * 这是典型的查询法/轮询法, 我不停的看这个值。
     * 所以需要升级为通知法, 即使用到条件变量这个机制。
     */
    while (anInstance->token <= 0) {
//        pause();
        pthread_mutex_unlock( &anInstance->mutex );
        sched_yield();
        pthread_mutex_lock( &anInstance->mutex );
    }

    int min = std::min( anInstance->token, tokens );
    // 这里马上就用了。
    anInstance->token -= min;
    pthread_mutex_unlock( &anInstance->mutex );

    return min;
}

int returnToken(Tbf* tbf, int tokens) {
    if (tokens <= 0) {
        return -1;
    }

    MyTbf* me = static_cast<MyTbf*>(tbf);

    pthread_mutex_lock( &me->mutex );
    me->token += tokens;
    if (me->token > me->burst) {
        me->token = me->burst;
    }
    pthread_mutex_unlock( &me->mutex );


    return tokens;
}
